using UnityEngine;

public class PlayerController : MonoBehaviour {
    [SerializeField] private float _moveSpeed = 5f;
    [SerializeField] private float _rollSpeed = 10f;
    [SerializeField] private float _sprintSpeed = 10f;

    [SerializeField] private float _rotateSpeed = 5f;
    [SerializeField] private float _speedChangeRate = 5f;

    [SerializeField] private float _jumpHeight = 2f;

    [SerializeField] private float _maxForce = 1f;

    [SerializeField]
    private PlayerInputs _input;

    [SerializeField]
    private Rigidbody _rb;

    [SerializeField]
    private Animator _animator;

    [SerializeField]
    private Transform _cameraTr;

    [SerializeField]
    private float _cameraRotateSpeed = 0.3f;

    [SerializeField]
    private Transform _cameraFollowTargetTr;

    [SerializeField]
    private Transform _groundChecker;

    [SerializeField]
    private LayerMask _groundLayerMask;

    private bool _rolling = false;

    private float _currentSpeed = 0;

    // move
    private Vector3 _currentMoveDirection = Vector3.zero;

    // jump and fall
    private Vector3 _verticalVelocity = Vector3.zero;

    private float _cameraRotateX = 0f;
    private float _cameraRotateY = 0f;

    private bool _isGround = false;
    public bool isGround => _isGround;

    private bool _isFalling = false;
    public bool isFalling => _isFalling;

    private void LateUpdate() {
        CameraRotation();
    }

    private void FixedUpdate() {
        GroundCheck();

        if (_input.rolling) {
            _rolling = !_rolling;

            _animator.SetBool("roll", _rolling);
        }

        Movement();

        CameraRotation();

        _input.ResetStatus();
    }

    private void GroundCheck() {
        _isGround = Physics.CheckSphere(_groundChecker.position, 0.2f, _groundLayerMask);
    }

    private void Movement() {
        ResetForce();

        Move(_rolling ? _rollSpeed : _input.sprinting ? _sprintSpeed : _moveSpeed);

        JumpAndFall();

        ApplyForce();
    }

    private void Move(float speed) {
        float thisFrameSpeed = _input.move.magnitude;
        if (thisFrameSpeed > 0.1f) {    // accelerate
            _currentMoveDirection = new Vector3(_input.move.x, 0f, _input.move.y);

            Quaternion rotation = RotateImpl(_currentMoveDirection);

            _currentMoveDirection = rotation * Vector3.forward;

            MoveImpl(_currentMoveDirection, thisFrameSpeed, speed);
        } else if (_currentSpeed > 0.1f) { // decelerate
            MoveImpl(_currentMoveDirection, 0f, speed);
        } else {
            _currentSpeed = 0f;

            _animator.SetFloat("speed", 0f);

            _rb.velocity = Vector3.zero;
        }
    }

    private Quaternion RotateImpl(Vector3 moveDir) {
        float rotationY = Quaternion.LookRotation(moveDir).eulerAngles.y + _cameraTr.rotation.eulerAngles.y;
        Quaternion rotation = Quaternion.Euler(0f, rotationY, 0f);

        transform.rotation = Quaternion.Lerp(transform.rotation, rotation, _rotateSpeed * Time.fixedDeltaTime);

        return rotation;
    }

    private void MoveImpl(Vector3 moveDir, float targetSpeed, float speed) {
        _currentSpeed = Mathf.Lerp(_currentSpeed, targetSpeed, _speedChangeRate * Time.fixedDeltaTime);

        // animation speed
        _animator.SetFloat("speed", _currentSpeed * speed);

        // target velocity
        Vector3 targetVelocity = _currentSpeed * speed * moveDir;

        // calculate force
        Vector3 velocityChange = (new Vector3(targetVelocity.x - _rb.velocity.x, 0f, targetVelocity.z - _rb.velocity.z));

        // limit force
        velocityChange = Vector3.ClampMagnitude(velocityChange, _maxForce);

        // velocity
        AddForce(velocityChange);
    }

    private void JumpAndFall() {
        bool updateVelocity = false;

        if (_isGround) {
            _isFalling = false;

            _verticalVelocity.y = 0f;
            if (_rolling && _input.jumping) {
                updateVelocity = true;

                _verticalVelocity.y = Mathf.Sqrt(_jumpHeight * 2 * -Physics.gravity.y); // h = v^2 / 2g
            }
        } else {
            _isFalling = true;

            updateVelocity = true;

            _verticalVelocity.y += Physics.gravity.y * Time.fixedDeltaTime;
        }

        if (updateVelocity) {
            // calculate force
            Vector3 velocityChange = new Vector3(0f, _verticalVelocity.y - _rb.velocity.y, 0f);

            // limit force
            velocityChange = Vector3.ClampMagnitude(velocityChange, 10f);

            AddForce(velocityChange);
        }
    }

    private Vector3 _velocity = Vector3.zero;

    private void ResetForce() {
        _velocity = Vector3.zero;
    }

    private void AddForce(Vector3 velocity) {
        _velocity += velocity;
    }

    private void ApplyForce() {
        _rb.AddForce(_velocity, ForceMode.VelocityChange);
    }

    private void CameraRotation() {
        _cameraRotateX += _input.look.y * _cameraRotateSpeed;
        _cameraRotateX = Mathf.Clamp(_cameraRotateX, -50f, 10f);

        _cameraRotateY += _input.look.x * _cameraRotateSpeed;

        _cameraFollowTargetTr.rotation = Quaternion.Euler(-_cameraRotateX, _cameraRotateY, 0f);
    }
}
